Типизация

В комментарии к  Разработка типовой системы  автор комментария использовал оборот «жёстко-типизованные языки». Видимо, автор имеет ввиду, что есть языки с разной «жесткостью» типизирования.

 

На мой взгляд, типизация не бывает разной жесткости — типизация, это всего лишь классификация данных. И она неизбежно должна быть во время выполнения. Речь может идти только в времени когда происходит типизация.

В условном языке с одним типом данных, типизация есть всегда. В классических языках со статической типизацией, типизация большей частью происходит во время компиляции. Не всегда, потому что для x.foo() в OOP языке компилятор проверяет только то, что в типе ‘x’ есть ‘foo’, но не может определить (в большинстве случаев) какая именно функция будет вызвана. В языках с утиной типизацией еще большая часть типизации сдвигается в сторону времени исполнения. И так до условного Javascript, в котором только во время исполнения становится известно есть ли в ‘foo’ в ‘x’, и можно ли его вызвать.

Если посмотреть на развитие языков, то в последнее время статические языки добавляют динамику (типизация во время исполнения), а динамические языки — статику, см. например, gradual typing.

Противопоставление статической и динамической типизации ушло в прошлое, вопрос стоит в правильном балансе. Вместо бинарного противопоставления есть шкала, с более-менее плавными переходами между вариантами типизации:

  1. Разработчик явно указал тип переменной — типизация во время написания текста (до компиляции).
  2. Вывод типов (type inference) — компилятор вывел тип переменной во время компиляции.
  3. Интерпретатор или среда выполнения (run-time) сделал типизацию в конкретной точке исполнения, определил/построил нужный тип (не только для динамических языков, но и для утиных интерфейсов, как в Go).
  4. JIT компилятор вывел множество допустимых типов , то есть построил тип объединения, в точке исполнения и породил код для этого типа.
  5. JIT компилятор использовал данные профилировщика и выполнил типизацию во время N-го выполнения (собирая данные во время первых N-1 выполнений).

А еще могут быть промежуточные точки, например, статический или динамический линкер провел анализ и определил типы. Или частичная типизация выполнена при конкретизации компоненты. Собственно, именно поэтому, я говорю о более-менее непрерывной шкале.

Вывод:

  • Типизация всегда есть, только выполняется в разное время, что влияет на эффективность/стабильность/полиморфность/гибкость кода.
  • При проектировании языка программирования нужно думать о балансе между временем типизации в соответствии с предполагаемой областью применения.
  • Семейство языков должно закрывать разные области применения, то есть иметь в составе языки с разным балансом времени типизации.

 

3 комментария


  1. «И она неизбежно должна быть во время выполнения.»
    Вовсе нет, Алексей. Типизация этапа исполнения — это полезная фича (я в вяло текущем режиме пытаюсь создать ВМ такого ЦП). Но современные подходы, недооценивающие важность типизации этапа исполнения имеют лишь зачаточную поддержку (например, секция кода для чтения и секция данных для чтения/записи, запрет исполнения данных и стека).
    Го, являясь с языком с утиной типизацией — на самом деле на этапе исполнения проверяет соответствие интерфейсов только при первом обращении. Динамическая типизация реализуема, но программным способом ,а не аппаратным ( а хотелось бы именно так для критичных задач).

    Да, аппаратная типизация дорого, и даже программный контроль типов на этапе исполнения может быть избыточен. Но типизация должна быть строгой с явным приведением типа как минимум на этапе компиляции. В противном случае будет ситуация как сейчас у меня на работе — хорошо, что тесты 96% код покрывают (сбойная память в кластере ВМ с высокой вероятностью) — по другому невозможно выявить, что за чертовщина происходит. А приложение про деньги. Потом бы в меня пальцем тыкали, что я не могу код писать без глюков.

    Ответить

    1. «типизация должна быть строгой с явным приведением типа как минимум на этапе компиляции» — для вашей задачи, готов согласится — должна быть на этапе компиляции. Но есть другие задачи, от одноразового «скрипта», который будет выполнен один раз прямо сейчас, и до задач, в которых важен полиморфизм.

      Руслан Богатырев как-то писал о необходимости разного «материала» для разных программ — для каких-то домиков достаточно соломы, а другие надо строить из железобетона, да еще и погрузить на 100 метров под землю.

      Необходимость семейство языков как раз и вытекает из того, что задачи очень разные и решать их надо с помощью разных инструментов. При этом, желательно чтобы часть инструментов все же была одинаковой или заменяемой. Помните эмблему Комкона — семигранную гайку?

      Хочется избежать того, что постоянно нужно сейчас — переходников с семигранной гайки на девятигранную.

      Ответить

  2. Я сначала возрадовался что столкнулся с эффективной реализацией duck typing , но концептуально она не вмещается в следующую схему
    type A
    foo ()
    precondition predicate1
    postcondition predicate2
    end // foo
    invariant
    predicate3
    end // type A
    type B
    foo ()
    precondition predicate4
    postcondition predicate5
    invariant
    predicate6
    end // type B

    foo (x: A) do
    x.foo ()
    end // foo
    foo (new B())
    Да мы можем на статике проверить что тип B имеет метод с именем foo и с нужной сигнатурой, а вот как быть с пред,пост условиями и инвариантом. Т.е. я не знаю способа как доказывать эквивалентность двух логичких утверждений (предиктов) на статике по сути по их тектсовому описанию. Т.е. передаваемый аргументы котрые валидны для foo из A вполен могут быть не валидны для foo и В котрое в резульатет мы будем пытаться вызывать.
    Да наследование implies coupling но оно увязывает опредленным образом пред,пост условия и инварианты — чего мне непонятно как достичь в случае с duck typing …
    Как всегда я не говорю нет, а просто ‘особое мнение’ :-)

    Ответить

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *